package com.gee.spring.boot.starter.opengemini.core.util;

import com.gee.spring.boot.starter.opengemini.core.annotation.Column;
import com.gee.spring.boot.starter.opengemini.core.annotation.MapColumn;
import com.gee.spring.boot.starter.opengemini.core.annotation.Measurement;
import com.gee.spring.boot.starter.opengemini.core.convertor.Convertor;
import com.gee.spring.boot.starter.opengemini.core.convertor.NoneConvertor;
import com.gee.spring.boot.starter.opengemini.core.domain.ColumnInfo;
import com.gee.spring.boot.starter.opengemini.core.domain.MeasurementInfo;
import com.gee.spring.boot.starter.opengemini.core.domain.PointWrapper;
import io.opengemini.client.api.Point;
import io.opengemini.client.api.QueryResult;
import io.opengemini.client.api.Series;
import io.opengemini.client.api.SeriesResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import static com.gee.spring.boot.starter.opengemini.core.util.ReflectionUtil.*;
import static com.gee.spring.boot.starter.opengemini.core.util.TimeUtil.nanosToTime;
import static com.gee.spring.boot.starter.opengemini.core.util.TimeUtil.timeToNanos;


/**
 * @author gepengjun
 * @since 2024/4/24 14:16
 */
public class OpengeminiUtil {

    private static final Logger logger = LoggerFactory.getLogger(OpengeminiUtil.class);

    //className MeasurementInfo
    public static final ConcurrentHashMap<String, MeasurementInfo> MeasurementInfoMap = new ConcurrentHashMap<>();

    //convertorClassName Convertor
    public static final ConcurrentHashMap<String, Convertor> ConvertorMap = new ConcurrentHashMap<>();


    public static final Duration durationOfDefaultZoneAndUtcZone;

    static {
        LocalDateTime now = LocalDateTime.now();
        ZoneId utc = ZoneId.of("UTC");
        ZonedDateTime utcTime = now.atZone(utc);
        ZonedDateTime defaultTime = now.atZone(ZoneId.systemDefault());
        durationOfDefaultZoneAndUtcZone = Duration.between(defaultTime.toInstant(), utcTime.toInstant());
    }

    /**
     * 将queryResult 转换为实体对象List
     * @param queryResult queryResult
     * @param clazz clazz
     * @return List<T>
     */
    public static <T> List<T> toPOJO(final QueryResult queryResult, final Class<T> clazz) {
        return toPOJO(queryResult, clazz, false);
    }

    public static <T> List<T> toPOJO(final QueryResult queryResult, final Class<T> clazz, boolean funcTime) {
        Objects.requireNonNull(queryResult, "queryResult");
        Objects.requireNonNull(clazz, "clazz");
        List<T> results = new LinkedList<T>();
        queryResult.getResults().stream()
                .filter(internalResult -> Objects.nonNull(internalResult) && Objects.nonNull(internalResult.getSeries()))
                .forEach(internalResult -> {
                    internalResult.getSeries().stream()
                            .forEachOrdered(series -> {
                                parsedSeries(series, clazz, results, funcTime);
                            });
                });
        return results;
    }

    private static <T> void parsedSeries(Series series, Class<T> clazz, List<T> results, boolean funcTime) {
        int columnSize = series.getColumns().size();
        MeasurementInfo measurementInfo = getMeasurementInfo(clazz);
        try {
            T object = null;
            for (List<Object> row : series.getValues()) {
                for (int i = 0; i < columnSize; i++) {
                    Object value = row.get(i);
                    if (value == null){
                        continue;
                    }
                    String columnName = series.getColumns().get(i);
                    if (object == null) {
                        object = newInstance(clazz);
                    }
                    if (funcTime){
                        if (columnName.equals(measurementInfo.getTimeFuncAlias())){
                            Field timeField = measurementInfo.getTimeField();
                            ReflectionUtil.setFieldValue(object, timeField, nanosToTime((Long) transformFieldValue(value, Long.class)));
                            continue;
                        }
                        //todo group by time 查询出来的的time字段暂时不进行处理
                        if ("time".equals(columnName)){
                            continue;
                        }
                    }
                    Field correspondingField = measurementInfo.getField(columnName);
                    if (correspondingField != null) {
                        setFieldValue(object, correspondingField, value);
                    }else if (measurementInfo.getMapColumnField() != null){
                        //动态column
                        setMapColumnFieldValue(object, measurementInfo.getMapColumnField(), columnName, value);
                    }
                }
                if (series.getTags() != null && !series.getTags().isEmpty()) {
                    for (Map.Entry<String, String> entry : series.getTags().entrySet()) {
                        Field correspondingField = measurementInfo.getField(entry.getKey());
                        if (correspondingField != null) {
                            setFieldValue(object, correspondingField, entry.getValue());
                        }
                    }
                }
                if (object != null) {
                    results.add(object);
                    object = null;
                }
            }
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private static <T> void setMapColumnFieldValue(T object, Field mapColumnField, String columnName, Object value) throws IllegalAccessException {
        Map fieldValue = (Map) getFieldValue(object, mapColumnField);
        if (fieldValue == null){
            fieldValue = new HashMap();
        }
        fieldValue.put(columnName, value);
        mapColumnField.set(object, fieldValue);
    }

    private static void setFieldValue(Object obj, Field field, Object value) throws IllegalAccessException {
        Column column = field.getAnnotation(Column.class);
        if (column.convertorClass() == NoneConvertor.class){
            field.set(obj, transformFieldValue(value, field.getType()));
        }else {
            Convertor convertor = getConvertor(column.convertorClass());
            Type saveType = getSaveType(column.convertorClass());
            field.set(obj,convertor.toJavaValue(transformFieldValue(value, saveType)));
        }
    }

    public static long getCount(QueryResult queryResult){
        List<SeriesResult> seriesResultList = queryResult.getResults();
        if (CollectionUtils.isEmpty(seriesResultList)){
            return 0;
        }
        SeriesResult seriesResult = seriesResultList.get(0);
        if (seriesResult == null){
            return 0;
        }
        List<Series> seriesList = seriesResult.getSeries();
        if (CollectionUtils.isEmpty(seriesList)){
            return 0;
        }
        Series series = seriesList.get(0);
        if (series == null){
            return 0;
        }
        List<List<Object>> seriesValues = series.getValues();
        if (CollectionUtils.isEmpty(seriesValues)){
            return 0;
        }
        List<Object> valueList = seriesValues.get(0);
        if (valueList.size() < 2){
            return 0;
        }
        Object countObj = valueList.get(1);
        return Double.valueOf(countObj.toString()).longValue();
    }

    private static Object transformFieldValue(Object value, Type type){
        if (type.equals(Long.class)) {
            return  new BigDecimal(value.toString()).longValue();
        } else if (type.equals(Integer.class)) {
            return  new BigDecimal(value.toString()).intValue();
        } else if (type.equals(Float.class)) {
            return  new BigDecimal(value.toString()).floatValue();
        } else if (type.equals(Double.class)) {
            return  new BigDecimal(value.toString()).doubleValue();
        } else if (type.equals(Boolean.class)) {
            return Boolean.valueOf(value.toString());
        }else if (type.equals(BigDecimal.class)) {
            return new BigDecimal(value.toString());
        } else if (type.equals(LocalDateTime.class)) {
            return parseStringToLocalDateTime(value.toString());
        } else if (type.equals(String.class)) {
            return value.toString();
        } else {
            throw new RuntimeException("common convertor unsupported field type: " + type.getTypeName());
        }
    }

    /**
     * 赋值
     *
     * @param obj
     * @param field
     * @param value
     * @throws IllegalAccessException
     */


    /**
     * 时间转换 数据库查出来的UTC时转为系统时间
     *
     * @param time UTC时
     * @return 系统时
     */
    public static LocalDateTime parseStringToLocalDateTime(String time) {
        DateTimeFormatter df = DateTimeFormatter.ISO_DATE_TIME;
        return LocalDateTime.parse(time, df).plusSeconds(durationOfDefaultZoneAndUtcZone.toSeconds());
    }

    public static PointWrapper toPointWrapper(Object object) {
        Class<?> clazz = object.getClass();
        throwExceptionIfMissingAnnotation(clazz);
        MeasurementInfo measurementInfo = getMeasurementInfo(object.getClass());
        List<Field> fieldList = measurementInfo.getFieldColumnInfoMap().values().stream()
                .map(ColumnInfo::getField)
                .filter(field -> getFieldValue(object, field) != null)
                .collect(Collectors.toList());
        Point.Builder pointBuilder = Point.newBuilder(measurementInfo.getMeasurement(object));
        for (Field field: fieldList){
            try {
                Column column = field.getAnnotation(Column.class);
                Object fieldValue = field.get(object);
                if ("time".equals(column.name())){
                    pointBuilder.time(timeToNanos((LocalDateTime) fieldValue));
                    continue;
                }
                if (column.tag()){
                    pointBuilder.tag(column.name(), fieldValue.toString());
                }else {

                    Class<? extends Convertor> convertorClass = column.convertorClass();
                    if (convertorClass == NoneConvertor.class){
                        pointBuilder.field(column.name(), fieldValue);
                    }else {
                        Object saveValue = getConvertor(convertorClass).toSaveValue(fieldValue);
                        pointBuilder.field(column.name(), saveValue);
                    }
                }

            } catch (IllegalAccessException e) {
                logger.error("to point error, field: {}, object: {}", field.getName(), object);
            }
        }

        Field mapColumnField = measurementInfo.getMapColumnField();
        if (mapColumnField != null){
            Map map = (Map) getFieldValue(object, mapColumnField);
            map.forEach((k, v) -> {
                pointBuilder.field(k.toString(), v);
            });
        }
        return PointWrapper.builder().point(pointBuilder.build()).database(measurementInfo.getDatabase(object)).build();
    }


    private static void throwExceptionIfMissingAnnotation(final Class<?> clazz) {
        if (!clazz.isAnnotationPresent(Measurement.class)) {
            throw new IllegalArgumentException(
                    "Class " + clazz.getName() + " is not annotated with @" + Measurement.class.getSimpleName());
        }
    }

    public static MeasurementInfo getMeasurementInfo(Class<?> entityClass){
        return MeasurementInfoMap.computeIfAbsent(entityClass.getName(), className -> {
            Measurement measurement = entityClass.getAnnotation(Measurement.class);
            if (measurement == null) {
                throw ExceptionUtil.re("class named %s needs an annotation with %s", className, Measurement.class.getName());
            }
            MeasurementInfo measurementInfo = new MeasurementInfo(measurement.measurement());
            measurementInfo.setDatabase(measurement.database());
            measurementInfo.setRetentionPolicy(measurement.retentionPolicy());
            List<String> measurementSuffixFieldNameList = Arrays.stream(measurement.measurementSuffixFields()).collect(Collectors.toList());
            String databaseSuffixField = measurement.databaseSuffixField();
            Map<String, ColumnInfo> fieldColumnInfoMap = measurementInfo.getFieldColumnInfoMap();
            Map<String, String> columnFieldMap = measurementInfo.getColumnFieldMap();
            Class<?> c = entityClass;
            Map<String, Boolean> measurementSuffixFieldCheckMap = measurementSuffixFieldNameList.stream()
                    .collect(Collectors.toMap(fieldName -> fieldName, fieldName -> false));
            while (c != null) {
                for (Field field : c.getDeclaredFields()) {
                    field.setAccessible(true);
                    if (measurementSuffixFieldCheckMap.containsKey(field.getName())){
                        measurementSuffixFieldCheckMap.put(field.getName(), true);
                    }
                    if (field.getName().equals(databaseSuffixField) ){
                        measurementInfo.setDatabaseSuffixField(field);
                    }
                    Column column = field.getAnnotation(Column.class);
                    if (column != null){
                        fieldColumnInfoMap.put(field.getName(),
                                new ColumnInfo(field, column.name(), column.tag(), column.funcAlias())
                        );
                        columnFieldMap.put(column.name(),field.getName());
                        if ("time".equals(column.name())){
                            measurementInfo.setTimeFuncAlias(column.funcAlias());
                            measurementInfo.setTimeField(field);
                        }
                    }
                    MapColumn mapColumn = field.getAnnotation(MapColumn.class);
                    if ( mapColumn != null && Map.class.isAssignableFrom(field.getType())){
                        measurementInfo.setMapColumnField(field);
                    }
                }
                c = c.getSuperclass();
            }
            List<String> notExistFieldList = measurementSuffixFieldNameList.stream()
                    .filter(fieldName -> !measurementSuffixFieldCheckMap.get(fieldName))
                    .collect(Collectors.toList());
            if (!CollectionUtils.isEmpty(notExistFieldList)){
                throw new RuntimeException("表名后缀字段不存在: " + notExistFieldList);
            }
            measurementInfo.setMeasurementSuffixFieldList(measurementSuffixFieldNameList);
            return measurementInfo;
        });
    }

    public static Convertor getConvertor(Class<? extends Convertor> convertorClass){
        return ConvertorMap.computeIfAbsent(convertorClass.getName(), className -> newInstance(convertorClass));
    }
}
